package server.rest;

import java.net.URI;
import java.security.GeneralSecurityException;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import common.KeyValue;

public class HttpApiClient implements StringContentHttpApiClient
{
    HttpClient[] clients;
    private final AtomicInteger index = new AtomicInteger();
    private final String bindToEndPoint;
    private HttpApiHeader[] defaultHeaders = null;
    final static int SOCKET_TIMEOUT = 105000;// The timeout is 105 seconds.
    
    private final static  String PUT_CATEGORY="HttpApiClient.Put";
    private final static  String POST_CATEGORY="HttpApiClient.Post";
    private final static  String DELETE_CATEGORY="HttpApiClient.Delete";
    private final static  String GET_CATEGORY="HttpApiClient.Get";

    public HttpApiClient() throws GeneralSecurityException
    {
        this(1, 100, 100, 1000, SOCKET_TIMEOUT, false, null, null, null);
    }
    
    public HttpApiClient(int maxClients, int maxConnections, int maxPerRoute, int samplingInterval, SSLSocketFactory socketFactory, HttpApiHeader[] defaultHeaders) throws GeneralSecurityException
    {
        this(maxClients, maxConnections, maxPerRoute, samplingInterval, SOCKET_TIMEOUT, false, socketFactory, defaultHeaders, null);
    }
    
    final Stack<HttpContext> httpContexts;
    public HttpApiClient(int maxClients, int maxConnections, int maxPerRoute, int samplingInterval, int socketTimeoutMs, boolean reuseHttpContext, SSLSocketFactory socketFactory, HttpApiHeader[] defaultHeaders, final String bindToEndPoint) throws GeneralSecurityException
    {
        if(reuseHttpContext)
        {
            httpContexts = new Stack<HttpContext>();
            for (int i = 0; i < maxClients*maxConnections; i++)
            {
                httpContexts.add(new BasicHttpContext());
            }
        }
        else
        {
            httpContexts =null;
        }
        
        this.defaultHeaders = defaultHeaders;
        this.bindToEndPoint = bindToEndPoint;
        
        this.clients = new HttpClient[maxClients];
        HttpParams params = new BasicHttpParams();
        params.setParameter("http.socket.timeout", socketTimeoutMs);
        
        for (int i = 0; i < maxClients; i++)
        {
            SchemeRegistry registry = new SchemeRegistry();
            registry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
            if (socketFactory != null)
            {
                registry.register(new Scheme("https", 443, socketFactory));
            }
            else
            {
               //TODO: implements SSLFactory
            }
            final ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(registry);
            cm.setMaxTotal(maxConnections);
            cm.setDefaultMaxPerRoute(maxPerRoute);
            this.clients[i] = new DefaultHttpClient(cm, params);
        }
    }

    public HttpContext waitForHttpContext() throws Exception
    {
        if (this.httpContexts == null)
        {
            return null;
        }
        synchronized (this.httpContexts)
        {
            if (this.httpContexts.size() ==0)
            {
                throw new Exception("Increase maxConnections or maxClients to create more httpContexts");
            }
            
            return this.httpContexts.pop();
        }
    }
    
    public void releaseHttpContext(HttpContext context)
    {
        if (this.httpContexts == null)
        {
            return;
        }
        synchronized (this.httpContexts)
        {
            this.httpContexts.push(context);
        }
    }
  
    public HttpApiResponse get(String categoryOverride, String remoteEndPoint, String pathAndQuery,
                    HttpApiHeader[] headers, Object... parameters) throws Throwable
    {
        String url = pathAndQuery != null ? remoteEndPoint + substituteParametersInPlaceHolders(pathAndQuery, parameters) : remoteEndPoint;
        validateRemoteEndPointFromUrl(url);
        
        if (categoryOverride == null)
        {
            categoryOverride = GET_CATEGORY + makeCategoryOverrideUrl(remoteEndPoint, pathAndQuery);
        }
        
      
            HttpContext httpContext = waitForHttpContext();
            try
            {
                HttpGet method = new HttpGet(url);
                if (defaultHeaders !=null)
                {
                    for (KeyValue<String, String> header : defaultHeaders)
                    {
                        method.setHeader(header.getKey(), header.getValue());
                    }
                }
                
                if (headers != null)
                {
                    for (KeyValue<String, String> header : headers)
                    {
                        method.setHeader(header.getKey(), header.getValue());
                    }
                }
                
                HttpApiResponse response = handleResponse(clients[Math.abs(index.incrementAndGet()%clients.length)].execute(method, httpContext));
         
                return response;
            }
            finally
            {
                releaseHttpContext(httpContext);
            }
        
    }

    public HttpApiResponse delete(String categoryOverride, String remoteEndPoint, String pathAndQuery,
                    HttpApiHeader[] headers, Object... parameters) throws Throwable
    {
        // TODO Auto-generated method stub
        return null;
    }

    public HttpApiResponse put(String categoryOverride, String remoteEndPoint, String pathAndQuery, String content,
                    String contentType, HttpApiHeader[] headers, Object... parameters) throws Throwable
    {
        // TODO Auto-generated method stub
        return null;
    }

    public HttpApiResponse post(String categoryOverride, String remoteEndPoint, String pathAndQuery, String content,
                    String contentType, HttpApiHeader[] headers, Object... parameters) throws Throwable
    {
        String url = pathAndQuery != null ? remoteEndPoint + substituteParametersInPlaceHolders(pathAndQuery,parameters): remoteEndPoint;

        if (categoryOverride == null)
        {
            categoryOverride = POST_CATEGORY + makeCategoryOverrideUrl(remoteEndPoint, pathAndQuery);
        }
        HttpContext httpContext = waitForHttpContext();
        try
        {
            HttpPost method = new HttpPost(url);
            if ((content != null) && (contentType != null))
            {
                StringEntity entity = new StringEntity(content, getCharset(contentType));
                entity.setContentType(contentType);
                method.setEntity(entity);
            }
            if (defaultHeaders!=null)
            {
                for (KeyValue<String, String> header : defaultHeaders)
                {
                    method.setHeader(header.getKey(), header.getValue());
                }
            }
            if (headers != null)
            {
                for (KeyValue<String, String> header : headers)
                {
                    method.setHeader(header.getKey(), header.getValue());
                }
            }
            HttpApiResponse response = handleResponse(clients[Math.abs(index.incrementAndGet())%clients.length].execute(method, httpContext));
            
            return response;
        }
        finally
        {
           releaseHttpContext(httpContext);
        }
    }
    
    
    
    public static String substituteParametersInPlaceHolders(String text, Object[] parameters ) throws Exception
    {
        if (parameters == null || parameters.length == 0)
        {
            // apparently URI.getEscapedPathQuery will return null rather than "" when input text is ""
            String uri = getRawPathQuery(text);
            return (uri == null ? "" : uri);
        }
        
        StringBuilder sb = new StringBuilder();
        int begin = 0;
        for (Object parameter : parameters)
        {
            int end = text.indexOf("??", begin);
            if (end == -1)
            {
                throw new Exception();
            }
            sb.append(text.substring(begin, end));
            sb.append(parameter.toString());
            begin = end + 2;
        }
        sb.append(text.substring(begin));
        return getRawPathQuery(sb.toString());
    }
    
    private static String getRawPathQuery(String uri)
    {
        return getRawPathQuery(URI.create(uri));
    }
    private static String getRawPathQuery(URI uri)
    {
        if (uri.getPath() == null && uri.getQuery() == null) {
            return null;
        }
        StringBuffer buff = new StringBuffer();
        if (uri.getPath() != null) {
            buff.append(uri.getPath());
        }
        if ( uri.getQuery() != null) {
            buff.append('?');
            buff.append( uri.getQuery());
        }
        return buff.toString();
    }
    String makeCategoryOverrideUrl(String remoteEndPoint,String pathAndQuery)
    {
        return " " + (pathAndQuery != null ? remoteEndPoint + pathAndQuery : remoteEndPoint);
    }
    
    private static String getRemoteEndPointFromUrl(final String url) throws Exception
    {
        int doubleslash = url.indexOf("//");
        if (doubleslash == -1)
        {
            throw new Exception("Invalid url =" + url + ". Missing '<protocol>'");
        }
        else
        {
            doubleslash += 2;
        }
        
        int end  = url.indexOf('/', doubleslash);
        if (end == -1)
        {
            end = url.indexOf('?', doubleslash);
        }
        end = end != 1 ? end : url.length();
        
        return url.substring(0, end);
    }
    
    private void validateRemoteEndPointFromUrl(String url) throws Exception
    {
        if (bindToEndPoint != null)
        {
            final String remoteEndPoint = getRemoteEndPointFromUrl(url);
            if (bindToEndPoint.equalsIgnoreCase(remoteEndPoint))
            {
                throw new Exception("This HttpApiClient is set to bind to endpoint=" + bindToEndPoint + " but is trying to hit " + remoteEndPoint);
            }
        }
    }

    public HttpApiResponse handleResponse(HttpResponse response)
    {
        int statusCode = response.getStatusLine().getStatusCode();
        try
        {
            HttpEntity entity = getProperEntity(response.getEntity());
            String content = null;
            String contentType = null;
            
            if (entity != null)
            {
                try
                {
                    Header contentTypeHeader = entity.getContentType();
                    if (contentTypeHeader != null)
                    {
                        contentType = contentTypeHeader.getValue();
                    }
                    
                    content = EntityUtils.toString(entity, getCharset(contentType));
                }
                catch (Exception e) {
                   throw e;
                }
                finally
                {
                    try
                    {
                        EntityUtils.consume(entity);
                    }
                    catch (Exception e)
                    {
                    }
                }
            }
            
            return new HttpApiResponse(content, contentType, statusCode, response);
        }
        catch (Exception e)
        {
            return new HttpApiResponse(null, null, statusCode, response);
        }
    }
    
    public static String getCharset(String contentType)
    {
        if (contentType != null)
        {
            for (String part : contentType.split(";"))
            {
                String[] pairs = part.split("=");
                if (pairs[0].trim().equalsIgnoreCase("charset"))
                {
                    return pairs[1].trim().toUpperCase();
                }
            }
        }
        return "UTF-8";
    }
    
    public static HttpEntity getProperEntity(HttpEntity entity)
    {
        HttpEntity decodedEntity = null;
                
        if (entity != null)
        {
            // Get content encoding string
            Header contentEncodingHeader = entity.getContentEncoding();
            String contentEncoding = "";
            if (contentEncodingHeader != null)
            {
                contentEncoding = contentEncodingHeader.getValue().toUpperCase();
            }
    
            // Set decoded entity to be the original request entity by default.
            decodedEntity = entity;
    
            // Add more decompressing methods here.
            if (contentEncoding.indexOf("GZIP") >= 0) // Content is in GZIP encoding
            {
                decodedEntity = new GzipDecompressingHttpEntity(entity);
            }
        }

        return decodedEntity;
    }
}
